{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h1 style=\"text-align: left;\">1. Introduction:</h1>\n",
    "Most tasks in Machine Learning can be reduced to classification tasks. For example, we have a medical dataset and we want to classify who has diabetes (positive class) and who doesn't (negative class). We have a dataset from the financial world and want to know which customers will default on their credit (positive class) and which customers will not (negative class).\n",
    "To do this, we can train a Classifier with a 'training dataset' and after such a Classifier is trained (we have determined  its model parameters) and can accurately classify the training set, we  can use it to classify new data (test set). If the training is done properly, the Classifier should predict the class probabilities of the new data with a similar accuracy.\n",
    "\n",
    "\n",
    "<img src=\"https://raw.githubusercontent.com/taspinar/siml/master/notebooks/img/classification.PNG\">\n",
    "\n",
    "\n",
    "There are three popular Classifiers which use three different mathematical approaches to classify data. Previously we have looked at the first two of these; <a href=\"http://ataspinar.com/2016/05/07/regression-logistic-regression-and-maximum-entropy-part-2-code-examples/\" target=\"_blank\">Logistic Regression</a> and the <a href=\"http://ataspinar.com/2016/02/15/sentiment-analysis-with-the-naive-bayes-classifier/\" target=\"_blank\">Naive Bayes</a> classifier. Logistic Regression uses a functional approach to classify data, and the Naive Bayes classifier uses a statistical (Bayesian) approach to classify data.<br><br>\n",
    "\n",
    "Logistic Regression assumes there is some function $ f(X) $ which forms a correct model of the dataset (i.e. it maps the input values correctly to the output values). This function is defined by its parameters $ \\theta_1, \\theta_2, ... $. We can use the gradient descent method to find the optimum values of these parameters.<br><br>\n",
    "\n",
    "The Naive Bayes method is much simpler than that; we do not have to optimize a function, but can calculate the <a href=\"https://en.wikipedia.org/wiki/Bayesian_probability\" target=\"_blank\">Bayesian (conditional) probabilities</a> directly from the training dataset. This can be done quiet fast (by creating a hash table containing the probability distributions of the features) but is generally less accurate.<br><br>\n",
    "\n",
    "Classification of data can also be done via a third way, by using a geometrical approach. The main idea is to find a line, or a plane, which can separate the two classes in their feature space. Classifiers which are using a geometrical approach are the Perceptron and the SVM (Support Vector Machines) methods.<br><br>\n",
    "\n",
    "<img src=\"https://raw.githubusercontent.com/taspinar/siml/master/notebooks/img/three_approaches.PNG\">\n",
    "\n",
    "Below we will discuss the Perceptron classification algorithm. Although Support Vector Machines is used more often, I think a good understanding of the Perceptron algorithm is essential to understanding Support Vector Machines and Neural Networks.<br><br>\n",
    "\n",
    "\n",
    "<h1 style=\"text-align: left;\">Introduction:</h1>\n",
    "\n",
    "The Perceptron is a lightweight algorithm, which can classify data quiet fast. But it only works in the limited case of a linearly separable, binary dataset. If you have a dataset consisting of only two classes, the Perceptron classifier can be trained to find a linear hyperplane which seperates the two. If the dataset is not linearly separable, the perceptron will fail to find a separating hyperplane.\n",
    "\n",
    "If the dataset consists of more than two classes we can use the standard approaches in multiclass classification <a href=\"https://en.wikipedia.org/wiki/Multiclass_classification#Transformation_to_Binary\" target=\"_blank\">(one-vs-all and one-vs-one</a>) to transform the multiclass dataset to a binary dataset. For example, if we have a dataset, which consists of three different classes:\n",
    "<ul>\n",
    " \t<li>In <strong>one-vs-all</strong>,  class I is considered as the positive class and the rest of the classes are considered as the negative class. We can then look for a separating hyperplane between class I and the rest of the dataset (class II and III). This process is repeated for class II and then for class III. So we are trying to find three separating hyperplanes; between class I and the rest of the data, between class II and the rest of the data, etc.\n",
    "If the dataset consists of K classes, we end up with K separating hyperplanes.</li>\n",
    " \t<li>In <strong>one-vs-one</strong>, class I is considered as the positive class and each of the other classes is considered as the negative class; so first class II is considered as the negative class and then class III is is considered as the negative class. Then this process is repeated with the other classes as the positive class.\n",
    "So if the dataset consists of K classes, we are looking for $ K(K-1)/2 $ separating hyperplanes.</li>\n",
    "</ul>\n",
    "\n",
    "Although the one-vs-one can be a bit slower (there is one more iteration layer), it is not difficult to imagine it will be more advantageous in situations  where a (linear) separating hyperplane does not exist between one class and the rest of the data, while it does exists between one class and other classes when they are considered individually. In the image below there is no separating line between the pear-class and the other two classes.\n",
    "\n",
    "![title](img/one-vs-one.png)\n",
    "\n",
    "Another difference is; If the dataset is not <a href=\"https://en.wikipedia.org/wiki/Linear_separability\" target=\"_blank\">linearly seperable</a> [<a href=\"http://www.ece.utep.edu/research/webfuzzy/docs/kk-thesis/kk-thesis-html/node19.html\" target=\"_blank\">2</a>] the perceptron will fail to find a separating hyperplane. The algorithm simply does not converge during its iteration cycle. The SVM on the other hand, can still find a maximum margin minimum cost decision boundary (a separating hyperplane which does not separate 100% of the data, but does it with some small error).\n",
    "\n",
    "\n",
    "<h1>2. The Perceptron Algorithm:</h1>\n",
    "It is often said that the perceptron is modeled after neurons in the brain. It has $ m $ input values (which correspond with the $ m $ features of the examples in the training set) and one output value. Each input value $ x_i $ is multiplied by a weight-factor $ w_i $. If the sum of the products between the feature value and weight-factor is larger than zero, the perceptron is activated and 'fires' a signal (+1). Otherwise it is not activated.\n",
    "\n",
    "![title](img/perceptron_schematic_overview.PNG)\n",
    "\n",
    "\n",
    "The weighted sum between the input-values and the weight-values, can mathematically be determined with the <a href=\"https://en.wikipedia.org/wiki/Dot_product\" target=\"_blank\">scalar-product</a> $ < w, x >$. To produce the behaviour of 'firing' a signal (+1) we can use the <a href=\"https://en.wikipedia.org/wiki/Sign_function\" target=\"_blank\">signum function</a> $ sgn() $; it maps the output to +1 if the input is positive, and it maps the output to -1 if the input is negative.\n",
    "\n",
    "Thus, this Perceptron can mathematically be modeled by the function $ y = sgn(b+ < w, x >) $. Here $ b $ is the <a href=\"http://stackoverflow.com/questions/2480650/role-of-bias-in-neural-networks/2499936#2499936\" target=\"_blank\">bias</a>, i.e. the default value when all feature values are zero.\n",
    "\n",
    "The perceptron algorithm looks as follows:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Perceptron():\n",
    "    \"\"\"\n",
    "    Class for performing Perceptron.\n",
    "    X is the input array with n rows (no_examples) and m columns (no_features)\n",
    "    Y is a vector containing elements which indicate the class \n",
    "        (1 for positive class, -1 for negative class)\n",
    "    w is the weight-vector (m number of elements)\n",
    "    b is the bias-value\n",
    "    \"\"\"\n",
    "    def __init__(self, b = 0, max_iter = 1000):\n",
    "        self.max_iter = max_iter\n",
    "        self.w = []\n",
    "        self.b = 0\n",
    "        self.no_examples = 0\n",
    "        self.no_features = 0\n",
    "    \n",
    "    def train(self, X, Y):\n",
    "        self.no_examples, self.no_features = np.shape(X)\n",
    "        self.w = np.zeros(self.no_features)\n",
    "        for ii in range(0, self.max_iter):\n",
    "            w_updated = False\n",
    "            for jj in range(0, self.no_examples):\n",
    "                a = self.b + np.dot(self.w, X[jj])\n",
    "                if np.sign(Y[jj]*a) != 1:\n",
    "                    w_updated = True\n",
    "                    self.w += Y[jj] * X[jj]\n",
    "                    self.b += Y[jj]\n",
    "            if not w_updated:\n",
    "                print(\"Convergence reached in %i iterations.\" % ii)\n",
    "                break\n",
    "        if w_updated:\n",
    "            print(\n",
    "            \"\"\"\n",
    "            WARNING: convergence not reached in %i iterations.\n",
    "            Either dataset is not linearly separable, \n",
    "            or max_iter should be increased\n",
    "            \"\"\" % self.max_iter\n",
    "                )\n",
    "\n",
    "    def classify_element(self, x_elem):\n",
    "        return int(np.sign(self.b + np.dot(self.w, x_elem)))\n",
    "            \n",
    "    def classify(self, X):\n",
    "        predicted_Y = []\n",
    "        for ii in range(np.shape(X)[0]):\n",
    "            y_elem = self.classify_element(X[ii])\n",
    "            predicted_Y.append(y_elem)\n",
    "        return predicted_Y\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, we set the bias-value and all the elements in the weight-vector to zero. Then we iterate 'max_iter' number of times over all the examples in the training set.\n",
    "\n",
    "Here, $Y[ii]$ is the actual output value of each training example. This is either +1 (if it belongs to the positive class) or -1 (if it does not belong to the positive class).,\n",
    "The activation function value $ a = b + < w, x >$ is the predicted output value. It will be $> 0 $ if the prediction is correct and $ < 0 $ if the prediction is incorrect. Therefore, if the prediction made (with the weight vector from the previous training example) is incorrect, $ sgn(y\\cdot a) $ will be -1, and the weight vector $ w $ is updated.\n",
    "\n",
    "If the weight vector is not updated after some iteration, it means we have reached convergence and we can break out of the loop. \n",
    "\n",
    "If the weight vector was updated in the last iteration, it means we still didnt reach convergence and either the dataset is not linearly separable, or we need to increase 'max_iter'.\n",
    "\n",
    "We can see that the Perceptron is an <a href=\"https://en.wikipedia.org/wiki/Online_algorithm\" target=\"_blank\">online algorithm</a>; it iterates through the examples in the training set, and for each example in the training set it calculates the value of the activation function $ a $ and updates the values of the weight-vector.\n",
    "\n",
    "<h2>2.2 Example: 2D</h2>\n",
    "Now lets examine the Perceptron algorithm for a linearly separable dataset which exists in 2 dimensions. For this we first have to create this dataset:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import random\n",
    "def generate_data(no_points):\n",
    "    X = np.zeros(shape=(no_points, 2))\n",
    "    Y = np.zeros(shape=no_points)\n",
    "    for ii in range(no_points):\n",
    "        X[ii][0] = random.randint(1,9)+0.5\n",
    "        X[ii][1] = random.randint(1,9)+0.5\n",
    "        Y[ii] = 1 if X[ii][0]+X[ii][1] >= 13 else -1\n",
    "    return X, Y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![title](img/2d_dataset.png)\n",
    "\n",
    "As we can see in the image above, the dataset contains two linearly separable classes, which are indicated with red and green dots. \n",
    "\n",
    "Using the Perceptron algorithm on this dataset, will result in.\n",
    "\n",
    "In the 2D case, the perceptron algorithm looks like:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Convergence reached in 54 iterations.\n",
      "[-1, 1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1, -1, 1]\n",
      "[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.\n",
      " 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.\n",
      " 0. 0.]\n"
     ]
    }
   ],
   "source": [
    "X, Y = generate_data(100)\n",
    "p = Perceptron()\n",
    "p.train(X, Y)\n",
    "\n",
    "X_test, Y_test = generate_data(50)\n",
    "predicted_Y_test = p.classify(X_test)\n",
    "print(predicted_Y_test)\n",
    "print(predicted_Y_test - Y_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we can see, the weight vector and the bias ( which together determine the separating hyperplane ) are updated when $ y_i \\cdot a $ is not positive.\n",
    "\n",
    "The result is nicely illustrated in this gif (reload if nothing if happening):\n",
    "\n",
    "<img src=\"https://github.com/taspinar/siml/raw/master/notebooks/img/perceptron.gif\">\n",
    "\n",
    "We can extend this to a dataset in any number of dimensions, and as long as it is linearly separable, the Perceptron algorithm will converge."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2>2.3 Kernel Perceptrons:</h2>\n",
    "\n",
    "One of the benefits of this Perceptron is that it is a very 'lightweight' algorithm; it is computationally very fast and easy to implement for datasets which are linearly separable. But if the dataset is not linearly separable, it will not converge.\n",
    "\n",
    "For such datasets, the Perceptron can still be used if the correct kernel is applied. In practice this is never done, and Support Vector Machines are used whenever a Kernel needs to be applied. Some of these Kernels are:\n",
    "<table>\n",
    "<tbody>\n",
    "<tr>\n",
    "<td width=\"256\">Linear:</td>\n",
    "<td width=\"256\">$ < w, x > $</td>\n",
    "</tr>\n",
    "<tr>\n",
    "<td width=\"256\">Polynomial:</td>\n",
    "<td width=\"256\">$ (<w, x> + c)^d $ with $ c >= 0$</td>\n",
    "</tr>\n",
    "<tr>\n",
    "<td width=\"256\">Laplacian RBF:</td>\n",
    "<td width=\"256\">$ exp(-\\lambda \\  |\\ w - x\\ | ) $</td>\n",
    "</tr>\n",
    "<tr>\n",
    "<td width=\"256\">Gaussian RBF:</td>\n",
    "<td width=\"256\">$ exp(-\\lambda \\ |\\ w - x\\ |^2 ) $</td>\n",
    "</tr>\n",
    "</tbody>\n",
    "</table>\n",
    "\n",
    "For more information about Kernel functions, a comprehensive list of kernels, and their source code, please <a href=\"http://crsouza.com/2010/03/17/kernel-functions-for-machine-learning-applications/\" target=\"_blank\">click here</a>.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
